#pragma once
#include <type_traits>
#include "type_traits.hpp"

namespace cpp{

	template<class T>
	class propagate_const;

	namespace __propagate_const{

		template<class T>
		struct __is_propagate_const :std::false_type
		{
		};

		template<class T>
		struct __is_propagate_const< propagate_const< T  > > :std::true_type
		{
		};

		template<class T>
		struct __is_pointer_to_object :std::bool_constant<
			std::is_pointer<T>::value
			&& !std::is_function< std::remove_pointer_t<T> >::value
			&& !std::is_void< std::remove_pointer_t<T> >::value
			&&  std::is_lvalue_reference< decltype(*std::declval<T&>())>::value
		>	{	};

		template<class T, class = void>
		struct __is_pointer_like_class :std::false_type
		{

		};

		template<class T>
		struct __is_pointer_like_class < T, std::void_t<
			decltype(std::declval<T&>().get()),
			decltype(std::declval<const T&>().get()),

			decltype(std::declval<T&>().operator->()),
			decltype(std::declval<const T&>().operator->()),

			decltype(bool(std::declval<T&>())),
			decltype(bool(std::declval<const T&>())),

			std::enable_if_t<std::is_lvalue_reference_v<decltype(*std::declval<T&>())> >
			>> : std::true_type
		{

		};

		template<class T>
		struct __is_pointer_to_object_or_pointer_like_class :std::bool_constant<
			__is_pointer_to_object<T>::value ||
			__is_pointer_like_class<T>::value
		>
		{

		};

		template<class T>
		inline auto _get(T* p)
		{
			return p;
		}

		template<class T>
		inline auto _get(T&& p)
		{
			return p.get();
		}
	}

	template<class T>
	class propagate_const
	{

		T _p;
	public:

		static_assert(__propagate_const::__is_pointer_to_object_or_pointer_like_class<T>::value, "T must be an object pointer type or a pointer-like class type");

		static_assert(std::is_lvalue_reference< decltype(*std::declval<T&>())>::value, "*std::declval<T&>() should be an lvalue reference type");

		using  element_type = std::remove_reference_t<decltype(*std::declval<T&>())>;

		constexpr propagate_const() = default;

		constexpr propagate_const(propagate_const&& p) = default;

		template<class U, class = std::enable_if_t< std::is_constructible<T, U&&>::value  && std::is_convertible<U&&, T>::value > >
		constexpr propagate_const(propagate_const<U>&& pu) :_p(std::move(pu._p))
		{
		}

		template<class U,
			class = std::enable_if_t< std::is_constructible<T, U&&>::value>,
			class = std::enable_if_t<!std::is_convertible<U&&, T>::value > >
			explicit constexpr propagate_const(propagate_const<U>&& pu) : _p(std::move(pu._p))
		{
		}


		template<class U,
			class = std::enable_if_t < std::is_constructible<T, U&&>::value
			&& std::is_convertible<U&&, T>::value
			&& !__propagate_const::__is_propagate_const< std::decay_t<U> >::value > >
			constexpr propagate_const(U&& u) :_p(std::forward<U>(u))
		{

		}

		template<class U,
			class = std::enable_if_t < std::is_constructible<T, U&&>::value>,
			class = std::enable_if_t < !std::is_convertible<U&&, T>::value
			&& !__propagate_const::__is_propagate_const<std::decay_t<U>>::value > >
			explicit constexpr propagate_const(U&& u) :_p(std::forward<U>(u))
		{

		}


		propagate_const(const propagate_const&) = delete;

		constexpr propagate_const& operator=(propagate_const&&) = default;

		template<class U, class = std::enable_if_t < std::is_convertible<U&&, T>::value> >
		constexpr propagate_const& operator=(propagate_const<U>&& pu)
		{
			_p = std::move(pu.t_);
			return *this;
		}

		template<class U, class =
			std::enable_if_t < std::is_convertible<U&&, T>::value
			&& !__propagate_const::__is_propagate_const< std::decay_t<U> >::value > >
			constexpr propagate_const& operator=(U&& u)
		{
			_p = std::forward<U>(u);
			return *this;
		}

		propagate_const& operator=(const propagate_const&) = delete;


		constexpr void swap(propagate_const & pt)
		{
			using std::swap;
			swap(_p, pt._p);
		}


		constexpr element_type* get()
		{
			return __propagate_const::_get(_p);
		}

		constexpr const element_type* get() const
		{
			return __propagate_const::_get(_p);

		}

		constexpr explicit operator bool() const
		{
			return static_cast<bool>(_p);
		}

		constexpr element_type& operator*()
		{
			return *get();
		}

		constexpr const element_type& operator*() const
		{
			return *get();
		}

		constexpr element_type* operator->()
		{
			return get();
		}

		constexpr const element_type* operator->() const
		{
			return get();
		}

		template<class = std::enable_if_t< __propagate_const::__is_pointer_to_object<T>::value || std::is_convertible<T, element_type* >::value> >
		constexpr operator element_type*()
		{
			return get();
		}

		template<class = std::enable_if_t< __propagate_const::__is_pointer_to_object<T>::value || std::is_convertible<T, element_type* >::value>>
		constexpr operator const element_type*() const
		{
			return get();
		}

	private:

		template<class U>
		friend constexpr  U& get_underlying(propagate_const<U>& pt);
	};



	template <class T>
	constexpr bool operator==(const propagate_const<T>& pt, nullptr_t)
	{
		return get_underlying(pt) == nullptr;
	}

	template <class T>
	constexpr bool operator==(nullptr_t, const propagate_const<T>& pt)
	{
		return pt == nullptr;
	}

	template <class T>
	constexpr bool operator!=(const propagate_const<T>& pt, nullptr_t)
	{
		return get_underlying(pt) != nullptr;
	}

	template <class T>
	constexpr bool operator!=(nullptr_t, const propagate_const<T>& pt)
	{
		return pt != nullptr;
	}


	template <class T, class U>
	constexpr bool operator==(const propagate_const<T>& pt, const propagate_const<U>& pu)
	{
		return get_underlying(pt) == get_underlying(pu);
	}

	template <class T, class U>
	constexpr bool operator!=(const propagate_const<T>& pt, const propagate_const<U>& pu)
	{
		return get_underlying(pt) != get_underlying(pu);

	}

	template <class T, class U>
	constexpr bool operator<(const propagate_const<T>& pt, const propagate_const<U>& pu)
	{
		return get_underlying(pt) < get_underlying(pu);
	}

	template <class T, class U>
	constexpr bool operator>(const propagate_const<T>& pt, const propagate_const<U>& pu)
	{
		return get_underlying(pt) > get_underlying(pu);
	}


	template <class T, class U>
	constexpr bool operator<=(const propagate_const<T>& pt, const propagate_const<U>& pu)
	{
		return get_underlying(pt) <= get_underlying(pu);
	}


	template <class T, class U>
	constexpr bool operator>=(const propagate_const<T>& pt, const propagate_const<U>& pu)
	{
		return get_underlying(pt) >= get_underlying(pu);
	}

	template <class T, class U>
	constexpr bool operator==(const propagate_const<T>& pt, const U& u)
	{
		return get_underlying(pt) == u;
	}


	template <class T, class U>
	constexpr bool operator!=(const propagate_const<T>& pt, const U& u)
	{
		return get_underlying(pt) != get_underlying(u);
	}


	template <class T, class U>
	constexpr bool operator<(const propagate_const<T>& pt, const U& u)
	{
		return get_underlying(pt) < u;
	}


	template <class T, class U>
	constexpr bool operator>(const propagate_const<T>& pt, const U& u)
	{
		return get_underlying(pt) > u;
	}

	template <class T, class U>
	constexpr bool operator<=(const propagate_const<T>& pt, const U& u)
	{
		return get_underlying(pt) <= u;
	}

	template <class T, class U>
	constexpr bool operator>=(const propagate_const<T>& pt, const U& u)
	{
		return get_underlying(pt) >= u;
	}

	template <class T, class U>
	constexpr bool operator==(const T& t, const propagate_const<U>& pu)
	{
		return t == get_underlying(pu);
	}

	template <class T, class U>
	constexpr bool operator!=(const T& t, const propagate_const<U>& pu)
	{
		return t != get_underlying(pu);
	}


	template <class T, class U>
	constexpr bool operator<(const T& t, const propagate_const<U>& pu)
	{
		return t < get_underlying(pu);
	}

	template <class T, class U>
	constexpr bool operator>(const T& t, const propagate_const<U>& pu)
	{
		return t > get_underlying(pu);
	}

	template <class T, class U>
	constexpr bool operator<=(const T& t, const propagate_const<U>& pu)
	{
		return t <= get_underlying(pu);
	}

	template <class T, class U>
	constexpr bool operator>=(const T& t, const propagate_const<U>& pu)
	{
		return t >= get_underlying(pu);
	}


	template< class T >
	constexpr T& get_underlying(propagate_const<T>& pt)
	{
		return pt._p;
	}

	template< class T >
	constexpr const T& get_underlying(const propagate_const<T>& pt)
	{
		return 	get_underlying(const_cast<propagate_const<T> &>(pt));
	}

	template< class T >
	constexpr void swap(cpp::propagate_const<T>& lhs, cpp::propagate_const<T>& rhs)
	{
		lhs.swap(rhs);
	}
}

